/**
 * FssRegistry.java
 */
package fss.remote;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.logging.Logger;

import fss.common.LookupServiceRegistry;
import fss.common.communication.PingRequest;
import fss.common.communication.QueryResult;
import fss.common.communication.Request;
import fss.core.config.FssProperties;
import fss.core.logger.FssLogger;
import fss.remote.util.RemoteFileService;
import fss.remote.util.RemoteFileServiceRegistry;
import fss.remote.util.RemoteServiceHeartbeater;
import fss.util.FssUtils;
import fss.util.UtcDate;
import fss.util.rmi.RmiId;
import fss.util.rmi.RmiUtils;

/**
 * A remote registry for service(s).
 * 
 * @author thinh
 *
 */
public class FssRegistry extends Observable implements ServiceRegistry, RemoteFileServiceRegistry {
    private static final long serialVersionUID = 1L;
    /**
     * The service poll time.
     */
    private int servicePollTime = -1;
    /**
     * Home directory for this service.
     */
    private String homedir = null;
    /**
     * A list of registered remote object identifiers.
     */
    private Set<RmiId> registration = new HashSet<RmiId>();
    /**
     * A cache of query results (id,filename).
     */
    private Set<QueryResult> queryCache = new HashSet<QueryResult>();
    /**
     * Heartbeats the registered remote service(s).
     */
    private RemoteServiceHeartbeater heartbeater = null;
    /**
     * The identifier for this remote service registry.
     */
    private RmiId id = null;
    /**
     * Constructor creating a service registry with specified id.
     * @param id the id.
     */
    public FssRegistry(RmiId id)  {
        this.id = id;
        homedir = FssProperties.getInstance().getServiceRegistryHomeDir(id.getName());
        FssUtils.createDirectory(homedir);
        servicePollTime = FssProperties.getInstance().getRemoteRegistryPollTime();
    }
    @Override
    public RmiId getId() throws RemoteException {
        return id;
    }
    @Override
    public void register(RemoteService service) throws RemoteException {
        RemoteServiceObserver ob = new RemoteServiceObserver(service);
        boolean added = registerLocal(service);
        // only register observer if it was successfully added to the local registration
        if(added) {
            super.addObserver(ob);
            FssLogger.getInstance().getServiceRegistryLogger(id.getName()).info("Registered service with id " + service.getId());
        } 
    }
    @Override
    public void deleteObserver(Observer observer) {
        if(observer instanceof RemoteServiceObserver) {
            RemoteServiceObserver o = (RemoteServiceObserver) observer;
            try {
                boolean removed = unregister(o.getId());
                FssLogger.getInstance().getServiceRegistryLogger(id.getName()).info("Unregistered " + o.getId() + "(" + removed + ")");
            } catch (RemoteException e) {
                FssLogger.getInstance().getServiceRegistryLogger(id.getName()).warning("Error unregistering remote service: " + e.toString());
            }
        }
        super.deleteObserver(observer);
    }
    @Override
    public void processRequest(Request request) throws RemoteException {
        if(request == null) {
            FssLogger.getInstance().getServiceRegistryLogger(id.getName()).fine("Received null request. Not processing.");
            return;
        }
        if(!(request instanceof PingRequest)) {
            FssLogger.getInstance().getServiceRegistryLogger(id.getName()).fine("Received " + request.getClass().getName() + " from " + request.getSource());
        }
        setChanged();
        notifyObservers(request);
    }
    @Override
    public Set<RmiId> getRegisteredServiceList() throws RemoteException {
        return registration;
    }
    @Override
    public void stop() throws RemoteException {
        if(heartbeater != null) {
            Thread _heartbeater = heartbeater;
            heartbeater = null;
            _heartbeater.interrupt();
        }
    }
    @Override
    public List<QueryResult> query(String param) throws RemoteException {
        Logger logger = FssLogger.getInstance().getServiceRegistryLogger(id.getName());
        UtcDate start = new UtcDate();
        List<QueryResult> results = new ArrayList<QueryResult>();
        List<RmiId> seen = new ArrayList<RmiId>();
        
        // look in cache first
        Set<QueryResult> _cached = queryCache;
        for(QueryResult qr : _cached) {
            if(qr.getFilename().equalsIgnoreCase(param)) {
                try {
                    // atempt to update the status
                    RemoteFileService service = null;
                    service = (RemoteFileService) RmiUtils.getRemoteObject(qr.getId());
                    int dcount = service.getDownloadCount();
                    int ucount = service.getUploadCount();
                    qr.setDownloadCount(dcount);
                    qr.setUploadCount(ucount);
                    
                    // update the cache 
                    addToQueryCache(qr);
                    // only add to the results if updating the count was successful
                    results.add(qr);
                    
                    if(!seen.contains(qr.getId())) {
                        seen.add(qr.getId());
                    }
                } catch (Exception e) {
                    logger.warning("Possible service down for query results " + qr + ". Excluding this value.");
                }
            }
        }
        logger.info("Query for " + param + " found (" + results.size() + ") from the local cache.");
        
        // search via configuration if not found in local cache
        if(results.size() == 0) {
            Map<RmiId, Set<RmiId>> allRegistry = LookupServiceRegistry.getAllServiceRegistry(seen);
            for(RmiId id : allRegistry.keySet()) {
                Set<RmiId> registration = allRegistry.get(id);
                for(RmiId info : registration) {
                    RemoteFileService fs = (RemoteFileService)RmiUtils.getRemoteObject(info);
                    List<QueryResult> rs = fs.query(param);
                    for(QueryResult file : rs) {
                        results.add(file);
                        // add to cache if not already exist
                        addToQueryCache(file);
                    }
                }
            }
        }
        UtcDate end = new UtcDate();
        float seconds = (end.getDate().getTime() - start.getDate().getTime()) / 1000f;
        FssLogger.getInstance().getServiceRegistryLogger(id.getName()).info("Query for '" + param + "' took " + seconds + " second(s)");
        return results;
    }
    @Override
    public void start() throws RemoteException {
        if(heartbeater == null) {
            Logger logger = FssLogger.getInstance().getServiceRegistryLogger(id.getName());
            StringBuilder m = new StringBuilder();
            m.append("\n********************************************************************************\n");
            m.append(id + " Started at " + new UtcDate() + "\n");
            m.append("********************************************************************************\n");
            logger.info(m.toString());
            heartbeater = new RemoteServiceHeartbeater(id, servicePollTime);
            heartbeater.start();
        }
    }
    @Override
    public void update(Object message) throws RemoteException {
        if(message instanceof QueryResult) {
            QueryResult qr = (QueryResult)message;
            if(addToQueryCache(qr)) {
                FssLogger.getInstance().getServiceRegistryLogger(id.getName()).info("Caching: " + qr);
            }
        }
    }
    /**
     * Register the remote service to the internal registration.
     * @param service the service.
     * @return true if registeration was completed; false otherwise.
     * @throws RemoteException error with registration.
     */
    private synchronized boolean registerLocal(RemoteService service) throws RemoteException {
        boolean added = false;
        try {
            // only add the registration and the observer if it's not already registered
            RmiId serviceId = service.getId();
            added = registration.add(serviceId);
            return added;
        } catch (RemoteException e) {
            FssLogger.getInstance().getServiceRegistryLogger(id.getName()).warning("Error registering remote service: " + e.toString());
            // do not add if something fails
            throw e;
        }
    }
    /**
     * The internal logic to unregister a remote service. 
     * <p>
     * This is used to remove observer objects.
     * @param service the remote service.
     * @throws RemoteException error removing the remote service.
     */
    private synchronized boolean unregister(RmiId id) throws RemoteException {
        boolean removed = true;
        if(registration.contains(id)) {
            removed = registration.remove(id);
        }
        return removed;
    }
    /**
     * Add to the local cache of queried/pinged file(s). 
     * @param qr the query result.
     * @return true if add; false otherwise.
     */
    private synchronized boolean addToQueryCache(QueryResult qr) {
        return queryCache.add(qr);
    }
}
